use crate::frb_generated::StreamSink;
use anyhow::{anyhow, Context, Result};
use git2::build::{CheckoutBuilder, RepoBuilder};
use git2::{Branch, CertificateCheckStatus, FetchOptions, Remote, RemoteCallbacks, Repository, RepositoryState};
use std::path::Path;
use std::sync::Arc;

const REPO_URL: &str = "https://gitcode.com/niuhuan_cn/genshin_wiki_data.git";

pub async fn init_root(root_path: String) -> Result<()> {
    crate::context::init_root(&root_path).await;
    Ok(())
}

pub fn repo_state() -> Result<String> {
    let repo_dir = crate::context::repo_dir();
    Ok(if std::fs::exists(repo_dir)? {
        match Repository::init(repo_dir) {
            Ok(repo) => {
                let state = repo.state();
                match state {
                    RepositoryState::Clean => "CLEAN".to_string(),
                    _ => "ERROR: REPO NOT CLEAN".to_string(),
                }
            }
            Err(err) => format!("ERROR:{}", err),
        }
    } else {
        "NotExists".to_string()
    })
}

pub fn re_clone(clone_stream_sink: StreamSink<String>) {
    let repo_dir = crate::context::repo_dir();
    if std::fs::exists(repo_dir).is_ok() {
        let _ = std::fs::remove_dir_all(repo_dir);
    }

    let arc_ss = Arc::new(clone_stream_sink);

    let ss = arc_ss.clone();
    let mut cb = RemoteCallbacks::new();
    cb.certificate_check(|_, _| {
        Ok(CertificateCheckStatus::CertificateOk)
    });
    cb.transfer_progress(move |stats| {
        let dc = stats.indexed_deltas();
        let oc = stats.indexed_objects();
        let _ = ss.add(format!("indexed [ deltas: {}, objects: {} ]", dc, oc));
        true
    });

    let ss = arc_ss.clone();
    let mut co = CheckoutBuilder::new();
    co.progress(move |_, cur, total| {
        let _ = ss.add(format!("checkout [ cur: {}, total: {} ]", cur, total));
    });

    let mut fo = FetchOptions::new();
    fo.remote_callbacks(cb);
    let result = match RepoBuilder::new()
        .fetch_options(fo)
        .with_checkout(co)
        .clone(REPO_URL, Path::new(repo_dir))
    {
        Ok(_) => "OK".to_string(),
        Err(err) => format!("ERROR:{}", err),
    };
    let _ = arc_ss.add(result);
}

pub fn repo_pull(fetch_stream_sink: StreamSink<String>) {
    let arc_ss = Arc::new(fetch_stream_sink);
    match fetch_inner(arc_ss.clone()) {
        Ok(_) => {
            let _ = arc_ss.add("OK".to_string());
        },
        Err(err) => {
            let _ = arc_ss.add(format!("ERROR:{}", err));
            return;
        }
    }
}

fn fetch_inner(arc_ss: Arc<StreamSink<String>>) -> Result<()> {
    let repo_dir = crate::context::repo_dir();
    let repo = Repository::open(repo_dir)?;
    /*
       Branch: Local: "master"
       Branch: Remote: "origin/HEAD"
       Branch: Remote: "origin/master"
    */
    let mut pull_branch: Option<Branch> = None;
    let mut branches = repo.branches(None)?;
    while let Some(branch) = branches.next() {
        let (branch, _) = branch?;
        if branch.is_head() {
            pull_branch = Some(branch);
            break;
        }
    }
    /*
    Remote: Some("origin")
    */
    let mut pull_remote: Option<Remote> = None;
    let remotes = repo.remotes()?;
    for remote in remotes.iter() {
        if let Some(remote) = remote {
            let remote = repo.find_remote(remote).expect("remote not found");
            pull_remote = Some(remote);
            break;
        }
    }
    //
    let branch = pull_branch.with_context(|| anyhow!("branch not found"))?;
    let branch = branch
        .name()?
        .with_context(|| anyhow!(""))?;
    let mut remote = pull_remote.with_context(|| anyhow!("remote not found"))?;
    // Fetch
    let ss = arc_ss.clone();
    let mut cb = git2::RemoteCallbacks::new();
    cb.certificate_check(|_, _| {
        Ok(CertificateCheckStatus::CertificateOk)
    });
    cb.transfer_progress(move |stats| {
        let dc = stats.indexed_deltas();
        let oc = stats.indexed_objects();
        let _ = ss.add(format!("indexed [ deltas: {}, objects: {} ]", dc, oc));
        true
    });
    let mut fo = FetchOptions::new();
    fo.remote_callbacks(cb);
    fo.download_tags(git2::AutotagOption::None);
    remote.fetch(&[branch], Some(&mut fo), None)?;
    let fetch_head = repo.find_reference("FETCH_HEAD")?;
    let fetch_commit = repo.reference_to_annotated_commit(&fetch_head)?;
    let analysis = repo.merge_analysis(&[&fetch_commit])?;
    if analysis.0.is_fast_forward() {
        println!("Doing a fast forward");
        let refname = format!("refs/heads/{}", branch);
        match repo.find_reference(&refname) {
            Ok(mut r) => {
                fast_forward(&repo, &mut r, &fetch_commit)?;
            }
            Err(_) => {
                // The branch doesn't exist so just set the reference to the
                // commit directly. Usually this is because you are pulling
                // into an empty repository.
                repo.reference(
                    &refname,
                    fetch_commit.id(),
                    true,
                    &format!("Setting {} to {}", branch, fetch_commit.id()),
                )?;
                repo.set_head(&refname)?;
                repo.checkout_head(Some(
                    git2::build::CheckoutBuilder::default()
                        .allow_conflicts(true)
                        .conflict_style_merge(true)
                        .force(),
                ))?;
            }
        };
    } else if analysis.0.is_normal() {
        // do a normal merge
        let head_commit = repo.reference_to_annotated_commit(&repo.head()?)?;
        normal_merge(&repo, &head_commit, &fetch_commit)?;
    } else {
        println!("Nothing to do...");
    }
    Ok(())
}

fn fast_forward(
    repo: &Repository,
    lb: &mut git2::Reference,
    rc: &git2::AnnotatedCommit,
) -> Result<(), git2::Error> {
    let name = match lb.name() {
        Some(s) => s.to_string(),
        None => String::from_utf8_lossy(lb.name_bytes()).to_string(),
    };
    let msg = format!("Fast-Forward: Setting {} to id: {}", name, rc.id());
    println!("{}", msg);
    lb.set_target(rc.id(), &msg)?;
    repo.set_head(&name)?;
    repo.checkout_head(Some(
        git2::build::CheckoutBuilder::default()
            // For some reason the force is required to make the working directory actually get updated
            // I suspect we should be adding some logic to handle dirty working directory states
            // but this is just an example so maybe not.
            .force(),
    ))?;
    Ok(())
}

fn normal_merge(
    repo: &Repository,
    local: &git2::AnnotatedCommit,
    remote: &git2::AnnotatedCommit,
) -> Result<(), git2::Error> {
    let local_tree = repo.find_commit(local.id())?.tree()?;
    let remote_tree = repo.find_commit(remote.id())?.tree()?;
    let ancestor = repo
        .find_commit(repo.merge_base(local.id(), remote.id())?)?
        .tree()?;
    let mut idx = repo.merge_trees(&ancestor, &local_tree, &remote_tree, None)?;

    if idx.has_conflicts() {
        println!("Merge conflicts detected...");
        repo.checkout_index(Some(&mut idx), None)?;
        return Ok(());
    }
    let result_tree = repo.find_tree(idx.write_tree_to(repo)?)?;
    // now create the merge commit
    let msg = format!("Merge: {} into {}", remote.id(), local.id());
    let sig = repo.signature()?;
    let local_commit = repo.find_commit(local.id())?;
    let remote_commit = repo.find_commit(remote.id())?;
    // Do our merge commit and set current branch head to that commit.
    let _merge_commit = repo.commit(
        Some("HEAD"),
        &sig,
        &sig,
        &msg,
        &result_tree,
        &[&local_commit, &remote_commit],
    )?;
    // Set working tree to match head.
    repo.checkout_head(None)?;
    Ok(())
}

pub fn repo_dir() -> Result<String> {
    Ok(crate::context::repo_dir().clone())
}
